package org.xtext.runtime.util;

import is.lill.acre.message.IACREAgentIdentifier;
import is.lill.acre.message.IACREMessage;

import java.util.HashMap;
import java.util.Map;

import org.xtext.runtime.acre.ACREService;
import org.xtext.runtime.formula.AND;
import org.xtext.runtime.formula.AcreHistory;
import org.xtext.runtime.formula.CartagoProperty;
import org.xtext.runtime.formula.Equal;
import org.xtext.runtime.formula.Goal;
import org.xtext.runtime.formula.GreaterThan;
import org.xtext.runtime.formula.GreaterThanOrEqual;
import org.xtext.runtime.formula.IFormula;
import org.xtext.runtime.formula.IMPLIES;
import org.xtext.runtime.formula.LessThan;
import org.xtext.runtime.formula.LessThanOrEqual;
import org.xtext.runtime.formula.LibraryFormula;
import org.xtext.runtime.formula.NOT;
import org.xtext.runtime.formula.NotEqual;
import org.xtext.runtime.formula.Predicate;
import org.xtext.runtime.formula.TRActive;
import org.xtext.runtime.interpreter.Agent;
import org.xtext.runtime.interpreter.NoSuchActionException;
import org.xtext.runtime.library.LibraryException;
import org.xtext.runtime.library.LibraryWrapper;
import org.xtext.runtime.term.Addition;
import org.xtext.runtime.term.CountTerm;
import org.xtext.runtime.term.Divide;
import org.xtext.runtime.term.ITerm;
import org.xtext.runtime.term.LibraryTerm;
import org.xtext.runtime.term.List;
import org.xtext.runtime.term.ListConstructor;
import org.xtext.runtime.term.Modulo;
import org.xtext.runtime.term.Multiply;
import org.xtext.runtime.term.Subtraction;
import org.xtext.runtime.term.Term;
import org.xtext.runtime.term.Variable;
import org.xtext.runtime.type.Type;
import org.xtext.runtime.type.TypeException;

public class EvaluateTermsVisitor implements ILogicVisitor {
	private Map<String, LibraryWrapper> libraries;
	private Agent agent;
	
	public EvaluateTermsVisitor(Agent agent) {
		this.libraries = agent.getLibraries();
		this.agent = agent;
	}
	
	public EvaluateTermsVisitor() {
		libraries = new HashMap<String, LibraryWrapper>();
	}

	public EvaluateTermsVisitor(Map<String, LibraryWrapper> libraries) {
		this.libraries = libraries;
	}

	@Override
	public IFormula visit(AND and) {
		IFormula[] formulae = new IFormula[and.size()];
//		System.out.println("and: " + and);
//		System.out.println("size: " + and.size());
		for (int i=0; i < formulae.length; i++) {
			formulae[i] = (IFormula) and.formulaAt(i).accept(this);
		}
		try {
//			System.out.println("and: " + new AND(formulae));
			return new AND(formulae);
		} catch (NullPointerException npe) {
			System.out.println("and: " + and);
			throw npe;
		}
	}

	@Override
	public IFormula visit(Equal equal) {
		return new Equal((ITerm) equal.left().accept(this), (ITerm) equal.right().accept(this));
	}

	@Override
	public IFormula visit(Goal goal) {
		return new Goal((Predicate) goal.formula().accept(this));
	}

	@Override
	public IFormula visit(GreaterThan gt) {
		try {
			return new GreaterThan((ITerm) gt.left().accept(this), (ITerm) gt.right().accept(this));
		} catch (TypeException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public IFormula visit(GreaterThanOrEqual gt) {
		try {
			return new GreaterThanOrEqual((ITerm) gt.left().accept(this), (ITerm) gt.right().accept(this));
		} catch (TypeException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public IFormula visit(IMPLIES implies) {
		return new IMPLIES((IFormula) implies.context().accept(this), (Predicate) implies.result().accept(this));
	}

	@Override
	public IFormula visit(LessThan lt) {
		try {
			return new LessThan((ITerm) lt.left().accept(this), (ITerm) lt.right().accept(this));
		} catch (TypeException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public IFormula visit(LessThanOrEqual lt) {
		try {
			return new LessThanOrEqual((ITerm) lt.left().accept(this), (ITerm) lt.right().accept(this));
		} catch (TypeException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public IFormula visit(NOT not) {
		return new NOT(not.sign(), (Predicate) not.formula().accept(this));
	}

	@Override
	public IFormula visit(NotEqual neql) {
		return new NotEqual((ITerm) neql.left().accept(this), (ITerm) neql.right().accept(this));
	}

	@Override
	public IFormula visit(Predicate predicate) {
//		System.out.println("formula: " + predicate);
		ITerm[] terms = new ITerm[predicate.size()];
		for (int i=0; i<terms.length; i++) {
//			System.out.println("\t" + predicate.termAt(i).getClass().getCanonicalName());
			terms[i] = (ITerm) predicate.termAt(i).accept(this);
//			System.out.println("\tafter: " + terms[i].getClass().getCanonicalName());
		}

		return new Predicate(predicate.identifier(), terms);
	}

	@Override
	public IFormula visit(TRActive active) {
		return active;
	}

	@Override
	public ITerm visit(Addition addition) {
//		System.out.println("processing: " + addition);
		if (addition.left().hasFreeVariables() || addition.right().hasFreeVariables()) return addition;

		ITerm l = (ITerm) Variable.rawValue(addition.left()).accept(this);
		ITerm r = (ITerm) Variable.rawValue(addition.right()).accept(this);
		
//		System.out.println("l: " + l + " / r: " + r);
		try {
			if (addition.type().equals(Type.integerType)) {
				return new Term<Integer>(Type.integerType, Type.termAsInt(l) + Type.termAsInt(r));
			} else if (addition.type().equals(Type.longType)) {
				return new Term<Long>(Type.longType, Type.termAsLong(l) + Type.termAsLong(r));
			} else if (addition.type().equals(Type.floatType)) {
				return new Term<Float>(Type.floatType, Type.termAsFloat(l) + Type.termAsFloat(r));
			} else if (addition.type().equals(Type.doubleType)) {
				return new Term<Double>(Type.doubleType, Type.termAsDouble(l) + Type.termAsDouble(r));
			} else if (addition.type().equals(Type.stringType)) {
				return new Term<String>(Type.stringType, Type.termAsString(l) + Type.termAsString(r));
			} else {
				System.err.println("[EvaluateTermsVisitor] WARNING: Unsupported type in visit(Addition): " + addition.type());
			}
		} catch (TypeException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public ITerm visit(Divide divide) {
		if (divide.left().hasFreeVariables() || divide.right().hasFreeVariables()) return divide;

		ITerm l = (ITerm) Variable.rawValue(divide.left()).accept(this);
		ITerm r = (ITerm) Variable.rawValue(divide.right()).accept(this);
		
		try {
			if (divide.type().equals(Type.integerType)) {
				return new Term<Integer>(Type.integerType, Type.termAsInt(l) / Type.termAsInt(r));
			} else if (divide.type().equals(Type.longType)) {
				return new Term<Long>(Type.longType, Type.termAsLong(l) / Type.termAsLong(r));
			} else if (divide.type().equals(Type.floatType)) {
				return new Term<Float>(Type.floatType, Type.termAsFloat(l) / Type.termAsFloat(r));
			} else if (divide.type().equals(Type.doubleType)) {
				return new Term<Double>(Type.doubleType, Type.termAsDouble(l) / Type.termAsDouble(r));
			} else {
				System.err.println("[EvaluateTermsVisitor] WARNING: Unsupported type in visit(Division): " + divide.type());
			}
		} catch (TypeException e) {
		}
		return null;
	}

	@Override
	public ITerm visit(LibraryTerm term) {
		try {
			LibraryWrapper wrapper = libraries.get(term.identifier());
//			System.out.println(term);
//			System.out.println((Predicate) term.function().accept(this));
			return Term.newInstance(wrapper.invokeTerm((Predicate) term.function().accept(this)));
		} catch (LibraryException e) {
			e.printStackTrace();
		} catch (NoSuchActionException e) {
			e.printStackTrace();
		}
		return null;
	}

	@Override
	public ITerm visit(List list) {
		ITerm[] terms = new ITerm[list.size()];
		for (int i = 0; i < terms.length; i++) {
			terms[i] = (ITerm) list.termAt(i).accept(this);
		}
		return new List(terms);
	}

	@Override
	public ITerm visit(ListConstructor constructor) {
		return constructor;
	}

	@Override
	public ITerm visit(Modulo modulo) {
		if (modulo.left().hasFreeVariables() || modulo.right().hasFreeVariables()) return modulo;

		ITerm l = (ITerm) Variable.rawValue(modulo.left()).accept(this);
		ITerm r = (ITerm) Variable.rawValue(modulo.right()).accept(this);

		try {
			if (modulo.type().equals(Type.integerType)) {
				return new Term<Integer>(Type.integerType, Type.termAsInt(l) % Type.termAsInt(r));
			} else if (modulo.type().equals(Type.longType)) {
				return new Term<Long>(Type.longType, Type.termAsLong(l) % Type.termAsLong(r));
			} else {
				System.err.println("[EvaluateTermsVisitor] WARNING: Unsupported type in visit(Modulo): " + modulo.type());
			}
		} catch (TypeException e) {
		}
		return null;
	}

	@Override
	public ITerm visit(Multiply multiply) {
		if (multiply.left().hasFreeVariables() || multiply.right().hasFreeVariables()) return multiply;

		ITerm l = (ITerm) Variable.rawValue(multiply.left()).accept(this);
		ITerm r = (ITerm) Variable.rawValue(multiply.right()).accept(this);
		
		try {
			if (multiply.type().equals(Type.integerType)) {
				return new Term<Integer>(Type.integerType, Type.termAsInt(l) * Type.termAsInt(r));
			} else if (multiply.type().equals(Type.longType)) {
				return new Term<Long>(Type.longType, Type.termAsLong(l) * Type.termAsLong(r));
			} else if (multiply.type().equals(Type.floatType)) {
				return new Term<Float>(Type.floatType, Type.termAsFloat(l) * Type.termAsFloat(r));
			} else if (multiply.type().equals(Type.doubleType)) {
				return new Term<Double>(Type.doubleType, Type.termAsDouble(l) * Type.termAsDouble(r));
			} else {
				System.err.println("[EvaluateTermsVisitor] WARNING: Unsupported type in visit(Multiply): " + multiply.type());
			}
		} catch (TypeException e) {
		}
		return null;
	}

	@Override
	public ITerm visit(Subtraction subtraction) {
		if (subtraction.left().hasFreeVariables() || subtraction.right().hasFreeVariables()) return subtraction;

		ITerm l = (ITerm) Variable.rawValue(subtraction.left()).accept(this);
		ITerm r = (ITerm) Variable.rawValue(subtraction.right()).accept(this);
		
		try {
			if (subtraction.type().equals(Type.integerType)) {
				return new Term<Integer>(Type.integerType, Type.termAsInt(l) - Type.termAsInt(r));
			} else if (subtraction.type().equals(Type.longType)) {
				return new Term<Long>(Type.longType, Type.termAsLong(l) - Type.termAsLong(r));
			} else if (subtraction.type().equals(Type.floatType)) {
				return new Term<Float>(Type.floatType, Type.termAsFloat(l) - Type.termAsFloat(r));
			} else if (subtraction.type().equals(Type.doubleType)) {
				return new Term<Double>(Type.doubleType, Type.termAsDouble(l) - Type.termAsDouble(r));
			} else {
				System.err.println("[EvaluateTermsVisitor] WARNING: Unsupported type in visit(Subtraction): " + subtraction.type());
			}
		} catch (TypeException e) {
		}
		return null;
	}

	@Override
	public ITerm visit(Term<?> term) {
		return term;
	}

	@Override
	public ITerm visit(Variable variable) {
		if (!variable.isFree()) return (ITerm) variable.value().accept(this);
		return variable;
	}

	@Override
	public IFormula visit(LibraryFormula formula) {
		LibraryWrapper wrapper = libraries.get(formula.identifier());

		try {
//			System.out.println(wrapper.invokePredicate((Predicate) formula.getFormula().accept(this)));
			return new Predicate("" + wrapper.invokePredicate((Predicate) formula.getFormula().accept(this)), new ITerm[] {});		} catch (LibraryException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return null;
	}

	@Override
	public ITerm visit(CountTerm countTerm) {
//		System.out.println("here: " + countTerm);
		java.util.List<Bindings> bindings = agent.queryAll(countTerm.formula());
		if (bindings == null) return Term.newInstance(0);
		
		return Term.newInstance(bindings.size());
	}

	@Override
	public IFormula visit(CartagoProperty cartagoProperty) {
		Predicate obs_prop = agent.getArtifactStore().getObservableProperty(cartagoProperty.getFormula().identifier());
		if (obs_prop == null) return cartagoProperty;
		
		Bindings b = Unifier.unify(obs_prop, cartagoProperty.getFormula());
		return (b == null) ? Predicate.FALSE : Predicate.TRUE;
	}

	@SuppressWarnings("unchecked")
	@Override
	public IFormula visit(AcreHistory acreHistory) {
		String cid = ((Term<String>) acreHistory.getConversationId().accept(this)).value();
		int index = ((Term<Integer>) acreHistory.getIndex().accept(this)).value();
		String type = ((Term<String>) acreHistory.getType().accept(this)).value();
		String performative = acreHistory.getPerformative();
		Predicate content = (Predicate) acreHistory.getContent().accept(this);
		
		// retrieve a message based on cid and index (implicitly checked)
		IACREMessage message = agent.getAcreMessage(cid, index);
		if (message == null) {
			// there is no matching message
//			System.out.println("no message matching: " + cid + " / " + index);
			return Predicate.FALSE;
		}
		
		// Now check the remaining fields of the message...
		
        // Check the performative
		if (!message.getPerformative().equals(performative)) return Predicate.FALSE;
//		System.out.println("matched the performative");
		
		// Check the type
		IACREAgentIdentifier participant = agent.getConversationManager().getOtherParticipant( agent.getConversationManager().getConversationByID(cid) );
        if ( type.equals("send") && message.getSender().equals( participant ) ) {
			return Predicate.FALSE;
		} else if ( type.equals("receive") && message.getReceiver().equals( participant ) ) {
			return Predicate.FALSE;
		}
//		System.out.println("matched the type");

		// Check the content...
        Predicate contnt = LogicUtilities.toPredicate(message.getContent());
        Bindings bindings = Unifier.unify(content, contnt);
//        if (bindings != null) System.out.println("matched the content..."); 
        return bindings == null ? Predicate.FALSE : Predicate.TRUE;
	}

}
